﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using Microsoft.WindowsAzure.StorageClient;
using System.Data.Services.Client;

namespace CPlase
{

    /// <summary>
    /// EntityQuery - Azure Storage provider witch supporting automaic sharding in multi-table and partitions
    /// One Input String and get Table Shard and partition
    /// </summary>
    public static class EQ1G
    {
        //public static bool IsDevMode = WebRole.IsDevStorage;

        /// <summary>
        /// Generate EntityQuery-object by input parameters
        /// </summary>
        public static EntityQuery Q(BitShardInfo BitShardInfo, string InputString, string RowKeyToSet = null)
        {
            return GetQuery(BitShardInfo.TabelName, InputString, BitShardInfo.ShardBitCount, BitShardInfo.PartitionBitCount, RowKeyToSet);
        }

        /// <summary>
        /// Generate EntityQuery-object by input parameters
        /// </summary>
        public static EntityQuery Query(BitShardInfo BitShardInfo, string InputString, string RowKeyToSet = null)
        {
            return GetQuery(BitShardInfo.TabelName, InputString, BitShardInfo.ShardBitCount, BitShardInfo.PartitionBitCount, RowKeyToSet);
        }

        /// <summary>
        /// Generate EntityQuery-object by input parameters
        /// </summary>
        public static EntityQuery GetQuery(BitShardInfo BitShardInfo, string InputString, string RowKeyToSet = null)
        {
            return GetQuery(BitShardInfo.TabelName, InputString, BitShardInfo.ShardBitCount, BitShardInfo.PartitionBitCount, RowKeyToSet);
        }

        /// <summary>
        /// Generate EntityQuery-object by input parameters
        /// </summary>
        public static EntityQuery GetQuery(string TableName, string InputString, BitShardInfo BitShardInfo, string RowKeyToSet = null)
        {
            return GetQuery(TableName, InputString, BitShardInfo.ShardBitCount, BitShardInfo.PartitionBitCount, RowKeyToSet);
        }

        /// <summary>
        /// Generate EntityQuery-object by input parameters
        /// </summary>
        /// <param name="TableName">String table name without sharding number</param>
        /// <param name="InputString">Input string will be processed to shard and Partition</param>
        /// <param name="ShardBitCount">Bit count for tables shards count. 4 meens 2^4 == 16 shards</param>
        /// <param name="PartitionBitCount">Bit count for partition in one shard. 5 meens 2^5 == 32 partitions in any shard</param>
        /// <param name="RowKeyToSet">If value is null RowKey will be assingned in InputString</param>
        /// <returns>EntityQuery-object</returns>
        public static EntityQuery GetQuery(string TableName, string InputString, byte ShardBitCount, byte PartitionBitCount, string RowKeyToSet = null)
        {
            EntityQuery Query = new EntityQuery();
            string MD5_AllBits = Security.GetHashMD5Binary(InputString, ShardBitCount + PartitionBitCount);
            if (ShardBitCount > 0)
            {
                string MD5_ShardBits = MD5_AllBits.Substring(0, ShardBitCount);
                Int32 ShardNumber = Convert.ToInt32(MD5_ShardBits, 2);
                Query.ShardNumber = ShardNumber;
            }
            string MD5_PartitionBits = Convert.ToString(Convert.ToInt32(MD5_AllBits.Substring(ShardBitCount, PartitionBitCount), 2), 16);

            Query.TableName = TableName;
            Query.PartitionKey = MD5_PartitionBits;
            Query.InputString = InputString;
            Query.RowKey = RowKeyToSet != null ? RowKeyToSet : InputString;
            return Query;
        }

        /// <summary>
        /// Gets one entity of T-class by EntityQuery-object
        /// </summary>
        public static T G<T>(TableServiceContext Context, EntityQuery EQ) where T : TableServiceEntity
        {
            return GetEntity<T>(Context, EQ);
        }

        /// <summary>
        /// Gets one entity of T-class by EntityQuery-object
        /// </summary>
        public static T Get<T>(TableServiceContext Context, EntityQuery EQ) where T : TableServiceEntity
        {
            return GetEntity<T>(Context, EQ);
        }

        /// <summary>
        /// Gets one entity of T-class by EntityQuery-object
        /// </summary>
        public static T GetEntity<T>(TableServiceContext Context, EntityQuery EQ) where T : TableServiceEntity
        {
            return GetEntity<T>(Context, EQ.TableName + EQ.ShardNumber, EQ.PartitionKey, EQ.RowKey);
        }

        /// <summary>
        /// Gets one entity of T-class by EntityQuery-object
        /// </summary>
        public static T GetEntity<T>(TableServiceContext Context, string TableName, string PartitionKey, string RowKey) where T : TableServiceEntity
        {
            var ContextQuery = Context.CreateQuery<T>(TableName);
            try
            {
                var Item = (from p in ContextQuery where p.PartitionKey == PartitionKey && p.RowKey == RowKey select p).First<T>();
                return Item;
            }
            catch (Exception Ex)
            {
                return null;
            }
        }

        public static IQueryable<T> GetListEntity<T>(TableServiceContext Context, string TableName, string PartitionKey) where T : TableServiceEntity
        {
            var ContextQuery = Context.CreateQuery<T>(TableName);
            try
            {
                var Item = (from p in ContextQuery where p.PartitionKey == PartitionKey select p);
                return Item;
            }
            catch
            {
                return null;
            }
        }

        public static IQueryable<T> GetListEntity<T>(TableServiceContext Context, EntityQuery EQ) where T : TableServiceEntity
        {
            var ContextQuery = Context.CreateQuery<T>(EQ.GetTable());
            try
            {
                var Items = (from p in ContextQuery where p.PartitionKey == EQ.PartitionKey select p);
                return Items;
            }
            catch
            {
                return null;
            }
        }

        /// <summary>
        /// Store one entity of T-class by EntityQuery-object
        /// </summary>
        public static bool Set<T>(TableServiceContext Context, EntityQuery EQ, T Entity)
        {
            return SaveEntity<T>(Context, EQ, Entity);
        }

        /// <summary>
        /// Store one entity of T-class by EntityQuery-object
        /// </summary>
        public static bool SetEntity<T>(TableServiceContext Context, EntityQuery EQ, T Entity)
        {
            return SaveEntity<T>(Context, EQ, Entity);
        }

        /// <summary>
        /// Store one entity of T-class by EntityQuery-object
        /// </summary>
        public static bool S<T>(TableServiceContext Context, EntityQuery EQ, T Entity)
        {
            return SaveEntity<T>(Context, EQ, Entity);
        }

        /// <summary>
        /// Store one entity of T-class by EntityQuery-object
        /// </summary>
        public static bool Save<T>(TableServiceContext Context, EntityQuery EQ, T Entity)
        {
            return SaveEntity<T>(Context, EQ, Entity);
        }

        /// <summary>
        /// Store one entity of T-class by EntityQuery-object
        /// </summary>
        public static bool SaveEntity<T>(TableServiceContext Context, EntityQuery EQ, T Entity)
        {
            try
            {
                Context.AddObject(EQ.TableName + EQ.ShardNumber, Entity);
                return true;
            }
            catch { }
            return false;
        }

        /// <summary>
        /// Store one entity of T-class by EntityQuery-object
        /// </summary>
        public static bool S<T>(TableServiceContext Context, EntityBind<T> EntityBind) where T : TableServiceEntity
        {
            return SaveEntity<T>(Context, EntityBind);
        }

        /// <summary>
        /// Store one entity of T-class by EntityQuery-object
        /// </summary>
        public static bool Save<T>(TableServiceContext Context, EntityBind<T> EntityBind) where T : TableServiceEntity
        {
            return SaveEntity<T>(Context, EntityBind);
        }

        /// <summary>
        /// Store one entity of T-class by EntityQuery-object
        /// </summary>
        public static bool SaveEntity<T>(TableServiceContext Context, EntityBind<T> EntityBind) where T : TableServiceEntity
        {
            try
            {
                Context.AddObject(EntityBind.GetTable(), EntityBind.Entity);
                return true;
            }
            catch { return false; }
        }

        /// <summary>
        /// Create EntityBind-object to contain entity and their EntityQuery-object
        /// </summary>
        public static EntityBind<T> B<T>(EntityQuery EQ, T Entity) where T : TableServiceEntity
        {
            return Bind<T>(EQ, Entity);
        }

        /// <summary>
        /// Create EntityBind-object to contain entity and their EntityQuery-object
        /// </summary>
        public static EntityBind<T> Bind<T>(EntityQuery EQ, T Entity) where T : TableServiceEntity
        {
            return new EntityBind<T>()
            {
                Entity = Entity,
                InputString = EQ.InputString,
                PartitionKey = EQ.PartitionKey,
                RowKey = EQ.RowKey,
                ShardNumber = EQ.ShardNumber,
                TableName = EQ.TableName
            };
        }

        /// <summary>
        /// Create EntityBind-object to contain entity and their EntityQuery-object
        /// </summary>
        public static EntityBind<T> GetBind<T>(EntityQuery EQ, T Entity) where T : TableServiceEntity
        {
            return Bind<T>(EQ, Entity);
        }
    }

    /// <summary>
    /// Object contains full information to access one entity.
    /// With EntityQuery-object EQ1G-processer can take entity
    /// or create new entity witch be available at this EntityQuery-information 
    /// </summary>
    public class EntityQuery
    {
        /// <summary>
        /// NUmber of sharded table
        /// </summary>
        public Int32 ShardNumber = 0;

        /// <summary>
        /// Table name without sharding number
        /// </summary>
        public string TableName;

        /// <summary>
        /// ParitionKey of entity
        /// </summary>
        public string PartitionKey;

        /// <summary>
        /// RowKey of entity
        /// </summary>
        public string RowKey;

        /// <summary>
        /// Стринг, с которого считались PK и ShardNumber
        /// </summary>
        public string InputString;

        /// <summary>
        /// Get finaly name of table with sharding number
        /// </summary>
        public string Tbl() { return GetTable(); }

        /// <summary>
        /// Get finaly name of table with sharding number
        /// </summary>
        public string GetTable()
        {
            return TableName + ShardNumber;
        }

        /// <summary>
        /// Set PartitionKey and RowKey to TableServiceEntity object
        /// </summary>
        public void Init(TableServiceEntity Entity)
        {
            Entity.PartitionKey = PartitionKey;
            Entity.RowKey = RowKey;
        }

        public bool Save<T>(TableServiceContext Context, T Entity)
        {
            return EQ1G.Save<T>(Context, this, Entity);
        }

        public DataServiceQuery<T> Query<T>(TableServiceContext Context)
        {
            return Context.CreateQuery<T>(GetTable());
        }
    }

    /// <summary>
    /// Container of Entity with their EntityQuery-object
    /// This is like Object with theis Data-passport :)
    /// </summary>
    /// <typeparam name="T">Class of entity to store. Must be extended from TableServiceEntity</typeparam>
    public sealed class EntityBind<T> : EntityQuery where T : TableServiceEntity
    {
        /// <summary>
        /// Data-Entity
        /// </summary>
        public T Entity;

        /// <summary>
        /// Store object with input TableServiceContext
        /// </summary>
        /// <returns>returns true if object was successably added to transaction</returns>
        public bool Save(TableServiceContext Context)
        {
            return EQ1G.Save<T>(Context, this);
        }
    }

    /// <summary>
    /// Information about data sharding in the cloud
    /// It need to access data with EQ1G-processer and to initializing of data storage
    /// </summary>
    public sealed class BitShardInfo
    {
        /// <summary>
        /// Bit count for tables shards count, 4 means 2^4 == 16 shards
        /// Value is using in development-mode
        /// </summary>
        private const byte ShardBitCountDev = 2;

        /// <summary>
        /// Bit count for partition in one shard, 3 means 2^3 == 8 shards
        /// Value is using in development-mode
        /// </summary>
        private const byte PartitionBitCountDev = 2;

        /// <summary>
        /// Bit count for tables shards count, 4 means 2^4 == 16 shards
        /// Value is using in cloud-deployment-mode
        /// </summary>
        private const byte ShardBitCountCloud = 6;

        /// <summary>
        /// Bit count for partition in one shard, 3 means 2^3 == 8 shards
        /// Value is using in cloud-deployment-mode
        /// </summary>
        private const byte PartitionBitCountCloud = 15;

        /// <summary>
        /// Constructor
        /// </summary>
        /// <param name="TabelName">Table name without sharding number</param>
        /// <param name="IsPrefixedName">if table name has variosly prefixes it need to set true. If no special needs - it must be false</param>
        /// <param name="PermanentShardBitCount">Bit count for tables shards count without any contitions will be set, no one setting cant break this value. If == 0 then it has no effect.</param>
        /// <param name="PermanentPartitionBitCount">Bit count for partition in one shard without any contitions will be set, no one setting cant break this value. If == 0 then it has no effect.</param>
        public BitShardInfo(string TabelName = "", bool IsPrefixedName = false,
            byte PermanentShardBitCount = 0,
            byte PermanentPartitionBitCount = 0, bool IsDevMode = true)
        {
            if (IsDevMode)
            {
                this._ShardBitCount = ShardBitCountDev;
                this._PartitionBitCount = PartitionBitCountDev;
            }
            else
            {
                this._ShardBitCount = ShardBitCountCloud;
                this._PartitionBitCount = PartitionBitCountCloud;
            }

            if (PermanentShardBitCount > 0) this._ShardBitCount = PermanentShardBitCount;
            if (PermanentPartitionBitCount > 0) this._PartitionBitCount = PermanentPartitionBitCount;

            this._TabelName = TabelName;
            this._IsPrefixedName = IsPrefixedName;
        }

        public int GetShardsCount()
        {
            int r = 1;
            for (int i = 0; i < _ShardBitCount; i++) r *= 2;
            return r;
        }

        private bool _IsPrefixedName = false;
        private byte _ShardBitCount = 1;
        private byte _PartitionBitCount = 2;
        private string _TabelName = "";

        /// <summary>
        /// true if TableName has addition prefix in name
        /// </summary>
        public bool IsPrefixedName { get { return _IsPrefixedName; } }

        /// <summary>
        /// Bit count for tables shards count, 4 means 2^4 == 16 shards
        /// </summary>
        public byte ShardBitCount { get { return _ShardBitCount; } }

        /// <summary>
        /// Bit count for partition in one shard, 3 means 2^3 == 8 shards
        /// </summary>
        public byte PartitionBitCount { get { return _PartitionBitCount; } }

        /// <summary>
        /// Table name without sharding number
        /// </summary>
        public string TabelName { get { return _TabelName; } }
    }
}